package edu.ccut.osgi.test;

import java.io.PrintStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import junit.framework.Test;
import junit.framework.TestResult;
import junit.framework.TestSuite;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;

/**
 * OSGi Junit Runner.
 */
public class OSGiJunitServiceImpl implements IOSGiJunitService {
	
	private static transient IOSGiJunitService instance = null;
	
	private static final String SUITE_METHODNAME = "suite";
	
	static {
		instance = new OSGiJunitServiceImpl();
	}
	
	private Map<Bundle, List<Test>> bundleTestSuites = new HashMap<Bundle, List<Test>>();

	private ResultPrinter printer = new ResultPrinter(System.out);
	
	private OSGiJunitServiceImpl() {
		
	}
	
	public static IOSGiJunitService getInstance() {
		return instance;
	}

	/* (non-Javadoc)
	 * @see edu.ccut.osgi.test.OSGiJunitService#addTestSuites(org.osgi.framework.Bundle)
	 */
	@SuppressWarnings("unchecked")
	public void addTestSuites(Bundle bundle) {
		LinkedList<String> paths = new LinkedList<String>();
		paths.add("/");

		while (!paths.isEmpty()) {
			String path = paths.removeLast();

			Enumeration<String> entrys = bundle.getEntryPaths(path);

			if (entrys != null) {
				while (entrys.hasMoreElements()) {
					String entry = entrys.nextElement();

					if (entry != null && entry.endsWith("/")) {
						paths.add(entry);
					} else if (entry != null && entry.endsWith(".class")) {
						int index = entry.lastIndexOf(".class");
						String className = entry.substring(0, index).replace("/", ".");
						
						try {
							addTestSuite(bundle, bundle.loadClass(className));
						} catch (ClassNotFoundException e) {
							// Nothing to do;
						}
					}
				}
			}
		}

	}

	private synchronized void addTestSuite(Bundle bundle, Class<?> test) {
		List<Test> list = bundleTestSuites.get(bundle);

		if (list == null) {
			list = new ArrayList<Test>();
			bundleTestSuites.put(bundle, list);
		}

		Test testCase = createTestFromClass(test, bundle);
		if (testCase != null) {
			list.add(testCase);
		}
	}

	/* (non-Javadoc)
	 * @see edu.ccut.osgi.test.OSGiJunitService#removeTestSuites(org.osgi.framework.Bundle)
	 */
	public void removeTestSuites(Bundle bundle) {
		bundleTestSuites.remove(bundle);
	}

	/* (non-Javadoc)
	 * @see edu.ccut.osgi.test.OSGiJunitService#setResultPrinter(java.io.PrintStream)
	 */
	public void setResultPrinter(PrintStream pw) {
		printer = new ResultPrinter(pw);
	}

	/* (non-Javadoc)
	 * @see edu.ccut.osgi.test.OSGiJunitService#run()
	 */
	public synchronized List<TestResult> run() {
		List<TestResult> results = new ArrayList<TestResult>(bundleTestSuites
				.size());

		for (Bundle bundle : bundleTestSuites.keySet()) {
			List<Test> tests = bundleTestSuites.get(bundle);

			for (Test test : tests) {
				TestResult tr = doRun(test);
				results.add(tr);
			}
		}

		return results;
	}

	private TestResult doRun(Test test) {
		TestResult result = new TestResult();
		result.addListener(printer);
		long startTime = System.currentTimeMillis();

		test.run(result);

		long endTime = System.currentTimeMillis();
		long runTime = endTime - startTime;
		printer.print(result, runTime);

		return result;
	}

	@SuppressWarnings("unchecked")
	private Test createTestFromClass(Class<?> clazz, Bundle bundle) {
		Method suiteMethod = null;
		boolean bc = false;
		try {
			suiteMethod = clazz.getMethod(SUITE_METHODNAME, new Class[0]);
		} catch (Exception e) {
			// try to use a suite method receiving a bundle context
			try {
				suiteMethod = clazz.getMethod(SUITE_METHODNAME,
						new Class[] { BundleContext.class });
				bc = true;
			} catch (Exception e2) {
				// try to extract a test suite automatically
				if (OSGiTestSuite.class.isAssignableFrom(clazz)) {
					OSGiTestSuite ts = new OSGiTestSuite((Class<Test>) clazz,
							getBundleContext(bundle));
					return ts;
				} else if (OSGiTestCase.class.isAssignableFrom(clazz)) {
					OSGiTestSuite ts = new OSGiTestSuite((Class<Test>) clazz,
							getBundleContext(bundle));
					return ts;
				} else if (Test.class.isAssignableFrom(clazz)){
					return new TestSuite(clazz);
				}
				
				return null;
			}
		}

		if (!Modifier.isStatic(suiteMethod.getModifiers())) {
			System.err.println("Suite() method must be static");
			return null;
		}
		Test test = null;
		try {
			if (bc) {
				test = (Test) suiteMethod.invoke(null,
						new Object[] { getBundleContext(bundle) }); // static
				// method
				// injection
				// the
				// bundle
				// context
			} else {
				test = (Test) suiteMethod.invoke(null, (Object[]) new Class[0]); // static
				// method
			}
		} catch (InvocationTargetException e) {
			System.err.println("Failed to invoke suite():"
					+ e.getTargetException().toString());
			return null;
		} catch (IllegalAccessException e) {
			System.err.println("Failed to invoke suite():" + e.toString());
			return null;
		}

		return test;
	}

	/* (non-Javadoc)
	 * @see edu.ccut.osgi.test.OSGiJunitService#getTests()
	 */
	public synchronized List<Test> getTests() {
		List<Test> results = new ArrayList<Test>();

		for (Bundle bundle : bundleTestSuites.keySet()) {
			List<Test> tests = bundleTestSuites.get(bundle);

			results.addAll(tests);
		}

		return results;
	}

	public TestResult run(Test test) {
		return doRun(test);
	}

	/* (non-Javadoc)
	 * @see edu.ccut.osgi.test.OSGiJunitService#getTests(long)
	 */
	public synchronized List<Test> getTests(long bundleId) {
		for (Bundle bundle : bundleTestSuites.keySet()) {
			if (bundle.getBundleId() == bundleId) {
				return bundleTestSuites.get(bundle);
			}
		}

		return null;
	}

	/* (non-Javadoc)
	 * @see edu.ccut.osgi.test.OSGiJunitService#run(long)
	 */
	public synchronized List<TestResult> run(long bundleId) {
		for (Bundle bundle : bundleTestSuites.keySet()) {
			if (bundle.getBundleId() == bundleId) {
				List<TestResult> results = new ArrayList<TestResult>();

				for (Test test : bundleTestSuites.get(bundle)) {
					TestResult tr = doRun(test);
					results.add(tr);
				}

				return results;
			}
		}

		return null;
	}

	public synchronized void stopping() {
		System.out.println("Cleaning test suites ...");
		bundleTestSuites.clear();
	}

	public void starting() {
		System.out.println("Junit Extender starting ...");
	}

	private BundleContext getBundleContext(Bundle bundle) {
		if (bundle == null) {
			return null;
		}

		// getBundleContext (OSGi 4.1)
		Method meth = null;
		try {
			meth = bundle.getClass()
					.getMethod("getBundleContext", new Class[0]);
		} catch (SecurityException e) {
			// Nothing do to, will try the Equinox method
		} catch (NoSuchMethodException e) {
			// Nothing do to, will try the Equinox method
		}

		// try Equinox getContext if not found.
		if (meth == null) {
			try {
				meth = bundle.getClass().getMethod("getContext", new Class[0]);
			} catch (SecurityException e) {
				// Nothing do to, will try field inspection
			} catch (NoSuchMethodException e) {
				// Nothing do to, will try field inspection
			}
		}

		if (meth != null) {
			if (!meth.isAccessible()) {
				meth.setAccessible(true);
			}
			try {
				return (BundleContext) meth.invoke(bundle, new Object[0]);
			} catch (IllegalArgumentException e) {
				err("Cannot get the BundleContext by invoking "
						+ meth.getName(), e);
				return null;
			} catch (IllegalAccessException e) {
				err("Cannot get the BundleContext by invoking "
						+ meth.getName(), e);
				return null;
			} catch (InvocationTargetException e) {
				err("Cannot get the BundleContext by invoking "
						+ meth.getName(), e);
				return null;
			}
		}

		// Else : Field inspection (KF and Prosyst)
		Field[] fields = bundle.getClass().getDeclaredFields();
		for (int i = 0; i < fields.length; i++) {
			if (BundleContext.class.isAssignableFrom(fields[i].getType())) {
				if (!fields[i].isAccessible()) {
					fields[i].setAccessible(true);
				}
				try {
					return (BundleContext) fields[i].get(bundle);
				} catch (IllegalArgumentException e) {
					err("Cannot get the BundleContext by invoking "
							+ meth.getName(), e);
					return null;
				} catch (IllegalAccessException e) {
					err("Cannot get the BundleContext by invoking "
							+ meth.getName(), e);
					return null;
				}
			}
		}
		err("Cannot find the BundleContext for " + bundle.getSymbolicName(),
				null);

		return null;
	}

	private void err(String s, Throwable e) {
		System.err.println(s + " : " + e.getMessage());
	}
}
